package com.yvon.boot.excel;

import com.alibaba.excel.context.AnalysisContext;
import com.alibaba.excel.event.AnalysisEventListener;
import lombok.Getter;
import lombok.extern.slf4j.Slf4j;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

/**
 * <p>
 * 由于在实际中可能会根据不同的业务场景需要的读取到的不同的excel表的数据进行不同操作,
 * 所以这里将ExcelListener作为所有listener的基类,根据读取不同的java模型自定义一个listener类继承ExcelListener,
 * 根据不同的业务场景选择性对以下方法进行重写
 * </p>
 *
 * <p>如果默认实现的方法不满足业务,则直接自定义一个listener实现AnalysisEventListener,重写一遍方法即可.</p>
 *
 * @author : Yvon
 * @since : 2021-07-05
 */
@Slf4j
public abstract class BaseExcelListener<Model> extends AnalysisEventListener<Model> {

    /**
     * 自定义用于暂时存储data。
     * 可以通过实例获取该值
     * 可以指定AnalysisEventListener的泛型来确定List的存储类型
     */
    @Getter
    private List<Model> data = new ArrayList<>();

    /**
     * 每隔N条存执行一次{@link BaseExcelListener#dataHandle()}方法,
     * 如果是入库操作,可使用默认的3000条,然后清理list,方便内存回收
     */
    private int batchCount = 3000;

    /**
     * @param batchCount see batchCount
     * @return this
     * @see BaseExcelListener#batchCount
     */
    public BaseExcelListener batchCount(int batchCount) {
        this.batchCount = batchCount;
        return this;
    }

    /**
     * <p>读取时抛出异常是否继续读取.</p>
     * <p>true:跳过继续读取 , false:停止读取 , 默认true .</p>
     */
    private boolean continueAfterThrowing = true;

    /**
     * 设置抛出解析过程中抛出异常后是否跳过继续读取下一行
     *
     * @param continueAfterThrowing 解析过程中抛出异常后是否跳过继续读取下一行
     * @return this
     */
    public BaseExcelListener continueAfterThrowing(boolean continueAfterThrowing) {
        this.continueAfterThrowing = continueAfterThrowing;
        return this;
    }

    /**
     * 读取过程中发生异常被跳过的错误记录
     * Integer 为 行号
     * String 为 错误信息
     */
    private Map<Integer, String> errorMsg = new HashMap<>();


    /**
     * 获取错误信息
     *
     */
    public Map<Integer, String> getErrorMsg() {
        return errorMsg;
    }

    /**
     * 每解析一行会回调invoke()方法。
     * 如果当前行无数据,该方法不会执行,
     * 也就是说如果导入的的excel表无数据,该方法不会执行,
     * 不需要对上传的Excel表进行数据非空判断
     *
     * @param object  当前读取到的行数据对应的java模型对象
     * @param context 定义了获取读取excel相关属性的方法
     */
    @Override
    public void invoke(Model object, AnalysisContext context) {
        log.info("解析到一条数据:{}", object);

        validateBeforeAddData(object);

        // 数据存储到list，供批量处理，或后续自己业务逻辑处理。
        data.add(object);

        //如果continueAfterThrowing 为false 时保证数据插入的原子性
        if (data.size() >= batchCount && continueAfterThrowing) {
            dataHandle();
            data.clear();
        }
    }

    /**
     * 该方法用于对读取excel过程中对每一行的数据进行校验操作,
     * 如果不需要对每行数据进行校验,则直接返回true即可.
     *
     * @param object 读取到的数据对象
     */
    public abstract void validateBeforeAddData(Model object);

    /**
     *
     * 处理数据
     * 对数据格式进行转换.
     */
    public abstract void dataHandle();


    /**
     * 解析监听器
     * 每个sheet解析结束会执行该方法
     *
     * @param context 定义了获取读取excel相关属性的方法
     */
    @Override
    public void doAfterAllAnalysed(AnalysisContext context) {
        dataHandle();
        log.info("当前sheet读取完毕, 读取错误的列表 : {}", getErrorMsg().toString());
        data.clear();//解析结束销毁不用的资源
    }

    /**
     * 在转换异常 获取其他异常下会调用本接口。抛出异常则停止读取。如果这里不抛出异常则继续读取下一行。
     * 如果不重写该方法,默认抛出异常,停止读取
     *
     * @param exception exception
     * @param context   context
     */
    @Override
    public void onException(Exception exception, AnalysisContext context) throws Exception {
        // 如果continueAfterThrowing为false,则直接将异常抛出
        if (!continueAfterThrowing) {
            throw exception;
        }

        Integer rowIndex = context.readRowHolder().getRowIndex();
        log.error("读取发生错误! 错误信息:{} ", rowIndex, exception);

        errorMsg.put(rowIndex, exception.getMessage());
    }

}
